import json
import os
import shutil
from pathlib import Path
from typing import Dict, List, Any, Optional

from logger import logger

LOGGER = logger.CustomLogger()

class MappingChecker:
    """
    简单映射检查器：
    - 若 normal_file 在 diff 中包含 mapping 的 key（函数名），且该出现位置向下 70 条 entry（含当前）内存在 new/old 修改行，
      则认为该函数被修改，必须在对应 multi-thread 文件中也包含 mapping 的 value（作为 new/old 出现）。
    - 否则认为该函数未被实际修改，忽略映射要求。
    """

    WINDOW = 70  # 向下检查的 entry 数量窗口

    def __init__(self, user_agent):
        self.user_agent = user_agent
        self.mapping_list = {}
        self.mapping_normal_file = {}
        self.load_mapping_file()

    def backup_mapping_file(self):
        try:
            file_path = f'config/{self.user_agent.replace("-", "_")}/multi_thread_mapping.json'
            if not os.path.exists(file_path):
                raise FileNotFoundError(f"Source file not found: {file_path}")

            base_name = os.path.basename(file_path)
            backup_name = f"{base_name}.bak"
            backup_path = os.path.join(os.path.dirname(file_path), backup_name)

            shutil.copy2(file_path, backup_path)
            LOGGER.info("[sys] Backup mapping file success.")
        except Exception as e:
            LOGGER.error(f"[sys] Backup mapping file failed: {str(e)}")
    
    def _restore_mapping_file(self, backup_path):
        try:
            # 验证备份文件存在且是.bak文件
            if not backup_path.endswith('.bak'):
                raise ValueError("必须提供.bak后缀的备份文件")
            if not os.path.exists(backup_path):
                raise FileNotFoundError(f"备份文件不存在: {backup_path}")

            # 生成原始文件名（去掉.bak）
            original_path = backup_path[:-4]

            # 执行恢复操作
            shutil.copy2(backup_path, original_path)
        except Exception as e:
            LOGGER.error(f"[sys] Restore mapping file failed: {str(e)}")

    def load_mapping_file(self) :
        """加载映射文件，返回映射列表"""
        mapping_path = f'config/{self.user_agent.replace("-", "_")}/multi_thread_mapping.json'
        try:
            with open(mapping_path, 'r', encoding='utf-8') as f:
                self.mapping_list = json.load(f)
                # 将mapping_list的normal_file全部去除构建一个set用于快速查找
                self.mapping_normal_file = set()
                for entry in self.mapping_list:
                    normal = entry.get("normal_file")
                    if normal:
                        self.mapping_normal_file.add(normal)
        except Exception as e:
            self._restore_mapping_file(mapping_path + ".bak")
            LOGGER.error(f"[sys] update {mapping_path} file failed, error details: {str(e)}")

    @staticmethod
    def _norm_path(p: Optional[str]) -> Optional[str]:
        if not p:
            return p
        return os.path.normpath(p).lstrip("./")

    @staticmethod
    def _extract_entry_text(entry: Any) -> str:
        """尽可能从多种 entry 结构中提取文本用于匹配"""
        if not isinstance(entry, dict):
            return ""
        for k in ("line_content", "line", "value", "text", "content"):
            v = entry.get(k)
            if not v:
                continue
            if isinstance(v, str):
                return v
            if isinstance(v, list):
                parts = []
                for it in v:
                    if isinstance(it, str):
                        parts.append(it)
                    elif isinstance(it, dict):
                        s = it.get("line_content") or it.get("line") or it.get("value") or ""
                        if s:
                            parts.append(s)
                if parts:
                    return "\n".join(parts)
            if isinstance(v, dict):
                for kk in ("line_content", "line", "value", "text"):
                    vv = v.get(kk)
                    if isinstance(vv, str) and vv:
                        return vv
                parts = [str(x) for x in v.values() if isinstance(x, str) and x]
                if parts:
                    return "\n".join(parts)
        # fallback join all string values
        parts = [str(x) for x in entry.values() if isinstance(x, str) and x]
        return "\n".join(parts) if parts else ""

    @staticmethod
    def _collect_entries(diff: Dict) -> List[Dict[str, Any]]:
        """
        返回 diff 中的条目列表，每项为字典 {'text': str, 'type': str}
        text 已尽力提取，type 为 entry.get('type') 或空串
        """
        entries = []
        for entry in diff.get("content", {}).get("text", []) or []:
            txt = MappingChecker._extract_entry_text(entry) or ""
            typ = entry.get("type") or ""
            entries.append({"text": txt, "type": typ})
        return entries

    @staticmethod
    def _any_mod_in_window(entries: List[Dict[str, Any]], start_idx: int, window: int) -> bool:
        """检查从 start_idx 开始（包含）向下 window 条目内是否存在 type 为 new/old 的修改行"""
        end = min(len(entries), start_idx + window)
        for i in range(start_idx, end):
            if entries[i].get("type") in ("new", "old"):
                return True
        return False

    def _key_is_actually_modified_in_diff(self, diff: Dict, key: str) -> bool:
        """
        判断 key 在 diff 中是否被“实际修改”：
        - 在 entries 中查找包含 key 的条目 index（按文本包含，大小写同时匹配）；
        - 对每个匹配位置，检查从该位置向下 WINDOW 条目（含当前）内是否存在 new/old；
        - 只要有一个位置满足就返回 True。
        """
        entries = self._collect_entries(diff)
        if not entries:
            return False
        key_lower = key.lower()
        for idx, e in enumerate(entries):
            txt = e.get("text", "")
            if not txt:
                continue
            # 精确子串匹配（同时支持大小写不敏感）
            if key in txt or key_lower in txt.lower():
                # 如果当前行本身就是修改行，也视为实际修改
                if e.get("type") in ("new", "old"):
                    return True
                # 向下窗口内查找修改
                if self._any_mod_in_window(entries, idx, self.WINDOW):
                    return True
        return False

    def _value_present_in_multi_diff(self, diff: Dict, val: Any) -> bool:
        """判断 multi-thread diff 中是否存在 value（在修改行中出现）"""
        if not diff:
            return False
        entries = self._collect_entries(diff)
        multi_texts = []
        for e in entries:
            if e.get("type") in ("new", "old"):
                multi_texts.append(e.get("text", ""))
        combined = "\n".join(multi_texts).lower()
        if isinstance(val, list):
            for vv in val:
                if isinstance(vv, str) and (vv in combined or vv.lower() in combined):
                    return True
            return False
        else:
            if not isinstance(val, str):
                val = str(val)
            return (val in combined) or (val.lower() in combined)

    def run(self, diff_data: Dict[str, Any]) -> Dict[str, Any]:
        """
        执行检查，返回结果字典：
          { "missing": [ { normal_file, normal_key, required_multi_file, required_multi_symbol, reason }, ... ] }
        """
        if self.mapping_list is None:
            self.load_mapping_file()
        # build lookup: normalized path -> diff entry
        diffs_by_path: Dict[str, Dict] = {}
        for d in diff_data.get("diffs", []) or []:
            p = d.get("statistic", {}).get("path") or d.get("statistic", {}).get("new_path") or d.get("statistic", {}).get("old_path")
            if not p:
                continue
            np = self._norm_path(p)
            diffs_by_path[np] = d

        missing = []
        for entry in self.mapping_list:
            normal = entry.get("normal_file")
            multi = entry.get("file")
            mapdict = entry.get("mapping") or {}
            if not normal or not mapdict:
                continue
            np = self._norm_path(normal)
            # normal file not changed -> skip
            if np not in diffs_by_path:
                continue
            diff_normal = diffs_by_path[np]
            for key, val in mapdict.items():
                if not key:
                    continue
                # 先判断 key 是否在 normal diff 中并且被“实际修改”（满足窗口规则）
                if not self._key_is_actually_modified_in_diff(diff_normal, key):
                    # 未实际修改或出现但下方无修改：忽略该 key
                    continue
                # 若实际修改，则要求 multi-thread 文件存在对应修改
                mp = self._norm_path(multi)
                diff_multi = diffs_by_path.get(mp)
                if not diff_multi:
                    missing.append({
                        "normal_file": normal,
                        "normal_key": key,
                        "required_multi_file": multi,
                        "required_multi_symbol": val,
                        "reason": "multi-thread file not present in diffs"
                    })
                    continue
                if not self._value_present_in_multi_diff(diff_multi, val):
                    missing.append({
                        "normal_file": normal,
                        "normal_key": key,
                        "required_multi_file": multi,
                        "required_multi_symbol": val,
                        "reason": "required symbol not modified in multi-thread file"
                    })
        return {"missing": missing}
